﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Collections.Concurrent;

using OrandoLabs.Common;
using OrandoLabs.Inquiro;
using OrandoLabs.EnduroStore;
using OrandoLabs.Inquiro.Parsers;
using OrandoLabs.Inquiro.EsentApi;

using HtmlAgilityPack;

namespace OrandoLabs.InquiroManager
{
    public class InquiroManager : IDisposable
    {
        int CurrentTarget;
        
        string Name;
        string DbName;
        public string IContextName;
        EnduroDatabase EDb;
        public InquiroDatabase IDb;
        OrandoInstance OInst;
        EnduroInstance EInst;
        EnduroStoreDatabase ESDb;
        InquiroInstance IInst;
        EnduroObjectStore Store;
        public DocumentManagerObject O;
        
        public InquiroManager(string name, string dir)
        {
            Name = name;
            DbName = name + ".edb";
            IContextName = "iqmgr_context_" + name;
            OInst = new OrandoInstance(name, dir);
            EInst = new EnduroInstance(OInst);
            EDb = new EnduroDatabase(EInst, DbName);
            ESDb = new EnduroStoreDatabase("iqmgr_store_" + name, EDb);
            IInst = new InquiroInstance(dir, InstanceIdWrapper.GetInstanceId(OInst));
            EsentApi api = new EsentApi();
            Inquiro.Api.SetApi(api);
            IDb = new InquiroDatabase(IInst, DbName);
            Store = new EnduroObjectStore(ESDb, "myp", "http://my.namespace.com");
            Store.DeclareObject<DocumentManagerObject>();
            Store.DeclareObject<DocumentObject>();
        }

        public static async Task<InquiroManager> CreateInquiroManager(string name, string dir)
        {
            return await Task<InquiroManager>.Run( async () =>
                {
                    InquiroManager manager = new InquiroManager(name, dir);
                    await manager.Initialize();
                    return manager;
                }
            );
        }

        public async Task<bool> Initialize()
        {
            O = await Store.FetchObjectFromIdAsync<DocumentManagerObject>(Name);
            if (O == null)
            {
                O = Store.NewObject<DocumentManagerObject>(Name);
                O.Name = Name;
                await Store.FlushAsync(false);
            }
            return true;
        }

        DocumentObject AddDocument()
        {
            Target target = new Target(CurrentTarget++);
            DocumentObject doc = Store.NewObject<DocumentObject>();
            doc.Target = target;
            O.AddDocument(doc);
            return doc;
        }
        
        public async Task<DocumentObject> AddHtmlDocument(string url, string friendlyName)
        {
            DocumentObject doc = AddDocument();
            doc.Url = url;
            doc.FriendlyName = friendlyName;
            await Store.FlushAsync(false);
            await ParseHtml(doc);
            return doc;
        }
        
        public void Dispose()
        {
            OInst.Dispose();
        }

        public DocumentObject FindTarget(Target target)
        {
            return O.DocumentsByTarget[target];
        }

        public async Task<bool> ParseHtml(DocumentObject doc)
        {
            return await Task<bool>.Run(async () =>
                {
                    HttpClient client = new HttpClient();
                    client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue("Mozilla", "5.0")));
                    HttpResponseMessage resp = await client.GetAsync(doc.Url);
                    string html = await resp.Content.ReadAsStringAsync();
                    HtmlDocument htmldoc = new HtmlDocument();
                    htmldoc.LoadHtml(html);
                    StringWriter text = new StringWriter();
                    ProcessNode(htmldoc.DocumentNode, text);
                    text.Flush();

                    InquiroTextTokenizer parser = new InquiroTextTokenizer();
                    using (InquiroContext ctx = new InquiroContext(IDb, IContextName))
                    {
                        parser.Parse(text.ToString());
                        parser.Compile(doc.Target);
                        ctx.AddTermsForTarget(parser.Terms.ToArray());
                    }
                    return true;
                }
            );
        }

        public void ProcessNode(HtmlNode node, StringWriter text)
        {
            string html;
            switch (node.NodeType)
            {
                case HtmlNodeType.Comment:
                    // don't output comments
                    break;

                case HtmlNodeType.Document:
                    ProcessChildren(node, text);
                    break;

                case HtmlNodeType.Text:
                    // script and style must not be output
                    string parentName = node.ParentNode.Name;
                    if ((parentName == "script") || (parentName == "style"))
                        break;

                    // get text
                    html = ((HtmlTextNode)node).Text;

                    // is it in fact a special closing node output as text?
                    if (HtmlNode.IsOverlappedClosingElement(html))
                        break;

                    // check the text is meaningful and not a bunch of whitespaces
                    if (html.Trim().Length > 0)
                    {
                        text.Write(" ");
                        text.Write(HtmlEntity.DeEntitize(html));
                        text.Write(" ");
                    }
                    break;

                case HtmlNodeType.Element:
                    switch (node.Name)
                    {
                        case "p":
                            text.Write("\n");
                            break;
                    }

                    if (node.HasChildNodes)
                    {
                        ProcessChildren(node, text);
                    }
                    break;
            }
        }
        
        private void ProcessChildren(HtmlNode node, StringWriter text)
        {
            foreach (HtmlNode subnode in node.ChildNodes)
            {
                ProcessNode(subnode, text);
            }
        }

    }

    [EnduroClass("documentmanager")]
    public class DocumentManagerObject : EnduroObject
    {
        ConcurrentDictionary<String, DocumentObject> DocumentsByUrlDictionary;
        ConcurrentDictionary<Target, DocumentObject> DocumentsByTargetDictionary;
        public DocumentManagerObject()
            : base()
        {
        }

        [EnduroProperty("name")]
        public string Name
        {
            get
            {
                return ToString("name");
            }
            set
            {
                SetValue("name", value);
            }
        }

        public void InitializeDictionaries()
        {
            if (DocumentsByTargetDictionary == null)
            {
                DocumentsByUrlDictionary = new ConcurrentDictionary<String, DocumentObject>();
                DocumentsByTargetDictionary = new ConcurrentDictionary<Target, DocumentObject>();
                List<DocumentObject> docs = ToList<DocumentObject>("documents");
                foreach (DocumentObject doc in docs)
                {
                    DocumentsByUrlDictionary[doc.Url] = doc;
                    DocumentsByTargetDictionary[doc.Target] = doc;
                }
            }
        }

        [EnduroProperty("documents")]
        public ConcurrentDictionary<Target,DocumentObject> DocumentsByTarget
        {
            get
            {
                InitializeDictionaries();
                return DocumentsByTargetDictionary;
            }
        }

        public ConcurrentDictionary<String, DocumentObject> DocumentsByUrl
        {
            get
            {
                InitializeDictionaries();
                return DocumentsByUrlDictionary;
            }
        }

        public void AddDocument(DocumentObject o)
        {
            InitializeDictionaries();
            DocumentsByUrlDictionary[o.Url] = o;
            DocumentsByTargetDictionary[o.Target] = o;
            AddObjectValue("documents", o);
        }

        public void RemoveDocument(DocumentObject o)
        {
            DocumentObject value;
            InitializeDictionaries();
            DocumentsByUrlDictionary.TryRemove(o.Url, out value);
            DocumentsByTargetDictionary.TryRemove(o.Target, out value);
            RemoveObjectValue("documents", o);
        }
    }

    [EnduroClass("document")]
    public class DocumentObject : EnduroObject, IComparable
    {
        public DocumentObject()
            : base()
        {
        }

        [EnduroProperty("url")]
        public string Url
        {
            get
            {
                return ToString("url");
            }
            set
            {
                SetValue("url", value);
            }
        }

        [EnduroProperty("friendlyname")]
        public string FriendlyName
        {
            get
            {
                return ToString("friendlyname");
            }
            set
            {
                SetValue("friendlyname", value);
            }
        }

        [EnduroProperty("target")]
        public Target Target
        {
            get
            {
                return new Target(ToInt("target"));
            }
            set
            {
                SetValue<int>("target", value.target);
            }
        }

        public override int GetHashCode()
        {
            return Target.target;
        }

        public override bool Equals(object obj)
        {
            DocumentObject that = obj as DocumentObject;
            return this.Target.target == that.Target.target;
        }

        public int CompareTo(object obj)
        {
            DocumentObject that = obj as DocumentObject;
            return this.Target.CompareTo(that.Target);
        }

    }
}
